Skip to content

feat(desktop): server tabs, several Codeman servers open in one window - #816

Open
Ark0N wants to merge 1 commit into
masterfrom
feat/desktop-server-tabs
Open

Ark0N wants to merge 1 commit into
masterfrom
feat/desktop-server-tabs

Conversation

@Ark0N

@Ark0N Ark0N commented Nov 14, 2026

Copy link
Copy Markdown
Owner

What this does

Codeman for Mac (packages/desktop, the native app: Rust, tao + wry/WKWebView + muda) showed one Codeman server at a time. Anyone with several machines (a Mac, a home server, a build box) had to use Switch Server and connect again each time. This PR gives every server its own tab in the window's title bar, and every tab stays live, so switching is one click.

  • Tabs in the title bar. The traffic lights sit at the left of a 38 pt tab strip. Each tab has a colour and a status badge: red count = sessions that need you, amber dot = waiting, blue ring = working, dashed circle = the server does not answer.
  • Opening servers. + or ⌘T opens the connect screen in a new tab. A server that is already open brings its tab forward instead of opening a second copy.
  • Rename, colour, reorder. Double-click a tab (or the right-click menu) to rename it; the menu also sets the colour, reloads, reconnects, closes, or closes the others. Tabs can be dragged.
  • Shortcuts (new Server menu). ⌘T, ⌘W, ⇧⌘T (reopen one of the last 10 closed tabs), ⌘0, ⇧⌘[ / ⇧⌘], ⌘1 to ⌘8, ⌘9 = last tab once there are nine. Close Window moves to ⇧⌘W. Ctrl+Tab and Alt+1..9 stay with Codeman's own session switching inside a tab.
  • ⌘0 overview. All servers at once, the ones that need you first, up to four sessions each with what they ask. Clicking a session opens its tab and selects that session.
  • Restore. Tabs are saved in desktop.json and come back at launch, connecting by themselves when auto-connect is on.
  • Logo and keep-awake. The lid logo blink follows all open servers; Keep This Mac Awake follows the tab that runs on this Mac.

How it works

  • One window. The strip is a small bundled page (assets/tabs.html) in its own child web view across the top. Each tab owns a connect view and, once connected, a server view that loads hidden and replaces the connect view on its first commit.
  • Badges come from the app, not the page: macOS stalls hidden web views, so one thread per tab reads GET /api/sessions every 2 s, backs off 60 s on a 429 and stops on 401/403 (no retry loop on a wrong password).
  • Logins are per tab: a Basic-auth challenge is answered only with the login of the tab whose WKWebView asked (macos.rs maps each web view to its tab).
  • Cookies are per server on macOS 14 and later: each server gets its own WKWebsiteDataStore (dataStoreForIdentifier with a stable id hashed from the server URL), so a server on :3000 and a beta on :5000 of the same host cannot log each other out. Before macOS 14 that API does not exist, so a second tab on a host that is already open is refused with a message.
  • One-time side effect: WebKit local storage lives in the data store, so only the server used last before this change keeps the default store and its per-device settings (font size, skin). Every other server starts those fresh once.
  • The strip and overview pages talk to Rust over IPC that policy.rs parses strictly and refuses from any frame other than that exact bundled page. The server never changes; the app is a client of the web UI like a browser tab.

Code map: new pure model src/tabs.rs (names, colours, order, shortcuts, strip/overview state); agents.rs reads per-session detail; config.rs stores tabs; menu.rs has the Server menu and the tab context menu; logo.rs watches all servers; main.rs holds the window and tab flows. Changeset included (patch).

Testing

  • Mac mini (macOS 26, Apple silicon): cargo fmt --check, 303 unit tests passed (9 ignored), clippy -D warnings clean, universal DMG builds and codesign --verify passes.
  • Linux cross-check: clippy for aarch64-apple-darwin with --locked, clean.
  • Browser tests (Playwright): test/desktop-tabs-pages.browser.test.ts (new, 3 walks over the strip and overview pages) and test/desktop-connect-page.browser.test.ts (new step for a restored tab), 8/8. Added to BROWSER_TEST_GLOBS.
  • An independent review pass found six issues, all fixed before this commit (among them: a login answered for the wrong tab, and two tabs racing to open the same server).

Not yet run in a real window (the Mac mini has no screen session). To check on a Mac before merging:

  • traffic lights centred in the strip
  • clicks on tabs reach the strip under the transparent title bar
  • dragging the strip moves the window, double-click zooms
  • typing goes back to the terminal after clicking the strip
  • renaming a tab by double-click
  • two servers logged in side by side (macOS 14+), and the refusal message before 14
  • tabs come back after quitting and relaunching

Known limits: toasts from the tab code have no zh-CN text yet; ⌘9 exists only from nine tabs on.

The Mac app showed one server at a time, and moving to another machine
meant Switch Server and a fresh connect. Now every server gets a tab in
the title bar, and every tab stays live.

- Tab strip: a small bundled page (assets/tabs.html) in its own web view
  across the top of the window, with the traffic lights inset into it.
  Each tab has a colour and a status badge: a red count when sessions
  need you, an amber dot when they are waiting, a blue ring while they
  work, a dashed circle while the server does not answer.
- The status comes from the app, not from the page: one thread per tab
  reads GET /api/sessions every 2 seconds (macOS stalls hidden web
  views), backs off for 60 s on a 429 and stops on 401/403, so a wrong
  password is never retried in a loop.
- + or ⌘T opens the connect screen in a new tab. A server that is
  already open just brings its tab forward instead of opening twice,
  also when two connects race (checked again when the probe ends).
- Double-click or the right-click menu renames a tab; the same menu
  sets the colour, reloads, reconnects, closes, or closes the others.
  Tabs can be dragged. A new Server menu carries ⌘T, ⌘W, ⇧⌘T (reopen
  closed tab, last 10), ⌘0, ⇧⌘[ / ⇧⌘] and ⌘1 to ⌘8 (⌘9 is the last
  tab once there are nine). Close Window moves to ⇧⌘W. Ctrl+Tab and
  Alt+1..9 stay with Codeman's own session switching.
- ⌘0 shows every server at once (assets/overview.html): the ones that
  need you first, up to four sessions each with what they ask, and a
  click on a session opens its tab and selects it.
- Tabs are saved in desktop.json (tabs, activeTab) and come back at
  launch, connecting by themselves when auto-connect is on.
- Logins: each tab keeps its own Basic credentials, and a challenge is
  answered only with the login of the tab whose web view asked
  (macos.rs binds each WKWebView to its tab).
- Cookies: on macOS 14 and later every server gets its own WebKit data
  store (WKWebsiteDataStore dataStoreForIdentifier, with a stable id
  hashed from the server URL), so a server on port 3000 and a beta on
  5000 of the same host cannot log each other out. Before macOS 14 a
  second tab on a host that is already open is refused with a message.
  The server used last before this change keeps the default store, so
  its per-device settings (font size, skin) survive; every other
  server starts those fresh once.
- The lid logo blink watches all open servers, and Keep This Mac Awake
  follows the tab that runs on this Mac.

New pure model in src/tabs.rs (names, colours, order, shortcuts, strip
and overview state) with unit tests; agents.rs reads the per-session
detail for the badges and the overview; policy.rs parses the strip and
overview IPC and refuses it from any other frame. Browser tests drive
both bundled pages (test/desktop-tabs-pages.browser.test.ts) and the
connect page's restored-tab path.

Not yet run in a real window: the Mac mini has no screen session. The
owner checks the traffic-light placement, clicks under the transparent
title bar, window drag and zoom from the strip, keyboard focus after
strip clicks, and two logged-in servers side by side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants